Skip to content

fix: suppress duplicate pending detection SSE broadcasts#2433

Merged
tphakala merged 3 commits intomainfrom
fix/pending-broadcast-dedup
Mar 20, 2026
Merged

fix: suppress duplicate pending detection SSE broadcasts#2433
tphakala merged 3 commits intomainfrom
fix/pending-broadcast-dedup

Conversation

@tphakala
Copy link
Copy Markdown
Owner

@tphakala tphakala commented Mar 20, 2026

Summary

  • Only broadcast pending detection snapshots when the snapshot actually differs from the last broadcast (new species, removed species, or increased hit count)
  • Previously every inference cycle broadcast the full snapshot even when nothing changed, spamming SSE clients with identical messages
  • Add hitCount field to SSEPendingDetection so the frontend can detect new inference hits for existing pending species
  • Update frontend diffPendingSnapshot() to treat increased hitCount as new activity (not just new species appearing)

Fixes https://git.koti/tphakala/birdnet-go/issues/20

Test Plan

  • Go unit tests: TestPendingSnapshotChanged (8 subtests), TestSnapshotVisiblePending_IncludesHitCount, TestBuildFlushNotification_IncludesHitCount
  • Frontend unit tests: detects increased hitCount as new activity, ignores unchanged hitCount (12 tests total pass)
  • golangci-lint run clean
  • Verify "currently hearing" card still updates when new species appear
  • Verify spectrogram overlay labels appear on new inference hits
  • Verify no repeated SSE messages for unchanged pending state (check browser DevTools Network tab)

Summary by CodeRabbit

  • New Features

    • Pending detections now include an optional activity count to track accumulated hits.
  • Improvements

    • Rising activity counts are treated as new activity for pending detections.
    • Duplicate pending-detection broadcasts are suppressed; ordering is stabilized for consistent updates.
  • Tests

    • Added tests covering activity-count handling, pending-diff behavior, and broadcast change detection.

Only broadcast pending detection snapshots when the snapshot actually
differs from the last broadcast (new species, removed species, or
increased hit count). Previously every inference cycle broadcast
the full snapshot even when nothing changed, spamming SSE clients
with identical messages.

- Add HitCount field to SSEPendingDetection for change detection
- Track last broadcast snapshot and skip identical broadcasts
- Update frontend diffPendingSnapshot to detect hitCount increases
- Add tests for snapshot change detection and HitCount propagation
@gemini-code-assist
Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly refines the real-time pending detection system by reducing redundant Server-Sent Events (SSE) broadcasts. By only sending updates when meaningful changes occur, it decreases network traffic and improves the efficiency of client-side updates. The changes also enhance the granularity of pending detection data, providing the frontend with more detailed information to react to ongoing inference activity.

Highlights

  • Optimized SSE Broadcasts: Implemented logic to suppress duplicate Server-Sent Events (SSE) broadcasts for pending detections, ensuring that snapshots are only sent when there are actual changes (new species, removed species, or increased hit counts).
  • Enhanced Pending Detection Data: Introduced a hitCount field to the SSEPendingDetection and PendingDetection types, allowing the frontend to track the number of inference hits for existing pending species.
  • Improved Frontend Reactivity: Updated the frontend's diffPendingSnapshot() utility to correctly identify an increased hitCount as new activity, preventing unnecessary re-renders when only the hit count changes.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 20, 2026

Caution

Review failed

Pull request was closed or merged during review

Walkthrough

Hit-count tracking was added across the stack: frontend types and overlay logic now handle an optional hitCount, new tests verify behavior, and backend SSE payloads include HitCount plus snapshot sorting and change-detection to avoid rebroadcasting identical pending snapshots.

Changes

Cohort / File(s) Summary
Type Definitions
frontend/src/lib/types/pending.types.ts
Added optional hitCount?: number to PendingDetection.
Frontend Detection Overlay & Tests
frontend/src/lib/utils/detectionOverlay.ts, frontend/src/lib/utils/detectionOverlay.test.ts
diffPendingSnapshot now uses a Map of previous entries to compare hitCount (treating missing as 0) and treats increased hitCount as new activity; added tests for increased vs unchanged hitCount.
Backend Pending Broadcast & Tests
internal/analysis/processor/pending_broadcast.go, internal/analysis/processor/pending_broadcast_test.go
Added HitCount int to SSEPendingDetection; introduced sortPendingSnapshot and pendingSnapshotChanged(prev,curr) to detect identical snapshots and suppress duplicate SSE emits; tests added for HitCount propagation and snapshot comparison scenarios.
Backend Processor Integration
internal/analysis/processor/processor.go
Added lastBroadcastSnapshot []SSEPendingDetection + mutex; flushPendingDetections now includes SourceID and HitCount on SSE entries and calls sortPendingSnapshot before broadcasting.

Sequence Diagram

sequenceDiagram
    participant Event as Detection Event
    participant Processor as Backend Processor
    participant Broadcaster as Broadcast Manager
    participant SSE as SSE Stream
    participant Frontend as Frontend Client
    participant Overlay as Detection Overlay

    Event->>Processor: increment pending Count
    Processor->>Broadcaster: flushPendingDetections() -> build []SSEPendingDetection{Species, SourceID, HitCount, Status, FirstDetected}
    Broadcaster->>Broadcaster: sortPendingSnapshot(curr)
    Broadcaster->>Broadcaster: pendingSnapshotChanged(prev, curr)?
    alt changed
        Broadcaster->>SSE: send SSEPendingDetection[]
        SSE->>Frontend: deliver snapshot
        Frontend->>Overlay: update pending snapshot
        Overlay->>Overlay: diffPendingSnapshot compares hitCount -> mark new activity if increased
    else unchanged
        Broadcaster-->>Broadcaster: skip send
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix: suppress duplicate pending detection SSE broadcasts' accurately summarizes the main objective of the changeset—preventing redundant SSE broadcasts when the pending detection snapshot hasn't changed.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/pending-broadcast-dedup

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

The pull request effectively addresses the issue of excessive SSE broadcasts by implementing a robust change detection mechanism for pending detection snapshots. The introduction of the hitCount field in both frontend and backend, along with the pendingSnapshotChanged function, ensures that updates are only sent when there's a meaningful change in species composition or activity. The added unit tests comprehensively cover the new logic, including various edge cases for snapshot comparison. The changes are well-implemented and contribute positively to the system's efficiency and maintainability.

Comment on lines 1335 to 1342
Status: PendingStatusActive,
FirstDetected: item.CreatedAt.Unix(),
Source: p.getDisplayNameForSource(item.Source),
SourceID: item.Source,
HitCount: item.Count,
})
}
}

This comment was marked as outdated.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in e152e50 — extracted sortPendingSnapshot helper and applied it to the flusher's broadcast snapshot before calling broadcastPendingSnapshot.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@internal/analysis/processor/pending_broadcast.go`:
- Around line 155-170: pendingSnapshotChanged does index-based comparisons
assuming both slices are identically ordered, but flushPendingDetections builds
broadcastSnapshot by iterating a map (non-deterministic order), causing false
positives; fix by sorting the broadcastSnapshot slice before calling
broadcastPendingSnapshot in flushPendingDetections (use the same ordering logic
as SnapshotVisiblePending or extract that comparator into a shared sort helper)
so pendingSnapshotChanged compares stable, identically-ordered slices.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 69c7c89d-3a1d-4dae-b4d9-d4110badf35d

📥 Commits

Reviewing files that changed from the base of the PR and between 3e18609 and 05afee5.

📒 Files selected for processing (6)
  • frontend/src/lib/types/pending.types.ts
  • frontend/src/lib/utils/detectionOverlay.test.ts
  • frontend/src/lib/utils/detectionOverlay.ts
  • internal/analysis/processor/pending_broadcast.go
  • internal/analysis/processor/pending_broadcast_test.go
  • internal/analysis/processor/processor.go

Comment thread internal/analysis/processor/pending_broadcast.go
Extract sortPendingSnapshot helper and apply it to the flusher's
broadcast snapshot. Map iteration order is non-deterministic in Go,
so the unsorted flusher snapshot caused false positives in the
index-based pendingSnapshotChanged comparison.
@tphakala
Copy link
Copy Markdown
Owner Author

/gemini review

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request effectively reduces unnecessary SSE broadcasts by comparing pending detection snapshots and only sending updates when there are actual changes. This is achieved by adding a hitCount to detections and updating both frontend and backend logic to handle it. The changes are well-tested. I've found one potential issue with the sorting logic that could lead to incorrect change detection in edge cases, which aligns with our guidelines on ensuring deterministic ordering, and have provided a suggestion to make it more robust.

Comment on lines +42 to +56
slices.SortFunc(s, func(a, b SSEPendingDetection) int {
if a.FirstDetected != b.FirstDetected {
if a.FirstDetected < b.FirstDetected {
return -1
}
return 1
}
if a.Species < b.Species {
return -1
}
if a.Species > b.Species {
return 1
}
return 0
})
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The current sorting logic in sortPendingSnapshot is not fully deterministic for the comparison performed in pendingSnapshotChanged. The comparison function checks SourceID, but the sort function does not use it as a tie-breaker.

If two detections have the same FirstDetected and Species but different SourceIDs, their relative order is not guaranteed. This can cause pendingSnapshotChanged to incorrectly report a change when the order of items happens to differ between snapshots, even if the set of items is identical.

To ensure a stable comparison, you should also sort by SourceID as a final tie-breaker. I've also taken the liberty to simplify the comparison logic slightly for better readability.

slices.SortFunc(s, func(a, b SSEPendingDetection) int {
		if a.FirstDetected < b.FirstDetected {
			return -1
		}
		if a.FirstDetected > b.FirstDetected {
			return 1
		}
		if a.Species < b.Species {
			return -1
		}
		if a.Species > b.Species {
			return 1
		}
		if a.SourceID < b.SourceID {
			return -1
		}
		if a.SourceID > b.SourceID {
			return 1
		}
		return 0
	})
References
  1. This aligns with the principle of ensuring deterministic ordering through explicit sorting to prevent non-deterministic behavior, as seen in the rule for thermal zones. Explicitly sorting by all relevant fields, including SourceID as a tie-breaker, ensures consistent comparisons and avoids spurious change detections.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in latest push — added SourceID as third-level tie-breaker in sortPendingSnapshot.

Comment on lines 114 to 122
}

// broadcastPendingSnapshot broadcasts a pending detection snapshot via the
// PendingBroadcaster callback. If no broadcaster is set, this is a no-op.
// PendingBroadcaster callback only when the snapshot differs from the last
// broadcast (new species, removed species, or updated hit counts).
// If no broadcaster is set, this is a no-op.
func (p *Processor) broadcastPendingSnapshot(snapshot []SSEPendingDetection) {
p.pendingBroadcasterMu.RLock()
broadcaster := p.PendingBroadcaster

This comment was marked as outdated.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — added SourceID as tie-breaker in sortPendingSnapshot.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@internal/analysis/processor/pending_broadcast.go`:
- Around line 38-56: The current comparator in sortPendingSnapshot (operating on
SSEPendingDetection) only uses FirstDetected and Species, which can leave ties
unresolved when multiple detections share the same Unix timestamp and species
and thus break pendingSnapshotChanged's index-based comparison; update the
comparator in sortPendingSnapshot to produce a total order by adding additional
tie-breakers (at least SourceID, and also include any other stable fields such
as DetectionID or Location if present on SSEPendingDetection) so that every pair
is deterministically ordered before pendingSnapshotChanged performs per-index
diffs.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 22e3c859-58cc-423f-9069-6a687061718e

📥 Commits

Reviewing files that changed from the base of the PR and between 05afee5 and e152e50.

📒 Files selected for processing (2)
  • internal/analysis/processor/pending_broadcast.go
  • internal/analysis/processor/processor.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • internal/analysis/processor/processor.go

Comment thread internal/analysis/processor/pending_broadcast.go
pendingSnapshotChanged compares SourceID but the sort didn't include
it, causing non-deterministic ordering with multiple sources.
@tphakala tphakala merged commit f9b4dbf into main Mar 20, 2026
23 of 28 checks passed
@tphakala tphakala deleted the fix/pending-broadcast-dedup branch March 20, 2026 15:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant